/*
 *  Copyright (C) 2012 Fishstix (Gene Ruebsamen - ruebsamen.gene@gmail.com)
 *  
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */
package com.fishstix.dosboxfree.joystick;

import com.fishstix.dosboxfree.touchevent.TouchEventWrapper;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class JoystickView extends View {
        public static final int INVALID_POINTER_ID = -1;
        
        // =========================================
        // Private Members
        // =========================================
        private final boolean D = false;
        String TAG = "JoystickView";
        
        private Paint dbgPaint1;
        private Paint dbgPaint2;
        
        private Paint bgPaint;
        private Paint handlePaint;
        
        private Paint butPaintA;
        private Paint butPaintB;
        private Paint butClickPaint;
        
        private int innerPadding;
        private int bgRadius;
        private int handleRadius;
        private int buttonRadius;
        private int movementRadius;
        private int handleInnerBoundaries;
        
        private JoystickMovedListener moveListener;
        private JoystickClickedListener clickListener;

        //# of pixels movement required between reporting to the listener
        private float moveResolution;

        private boolean yAxisInverted;
        private boolean autoReturnToCenter;
        
        //Max range of movement in user coordinate system
        public final static int CONSTRAIN_BOX = 0;
        public final static int CONSTRAIN_CIRCLE = 1;
        private int movementConstraint;
        private float movementRange;

        public final static int COORDINATE_CARTESIAN = 0;               //Regular cartesian coordinates
        public final static int COORDINATE_DIFFERENTIAL = 1;    //Uses polar rotation of 45 degrees to calc differential drive paramaters
        private int userCoordinateSystem;
        
        //Records touch pressure for click handling
        private boolean clickedJoy=false,clickedA=false,clickedB=false;
        private float clickThreshold;
        
        //Last touch point in view coordinates
        private int pointerId = INVALID_POINTER_ID;
        private int pointerId_butA = INVALID_POINTER_ID;
        private int pointerId_butB = INVALID_POINTER_ID;
        private float touchX, touchY;
        
        //Last reported position in view coordinates (allows different reporting sensitivities)
        private float reportX, reportY;
        
        //Handle center in view coordinates
        private float handleX, handleY;
        
        //Center of the view in view coordinates
        private int cX, cY;
        
        //Center of button A
        private int cbutAX,cbutAY;
        
        //Center of button B
        private int cbutBX,cbutBY;

        //Size of the view in view coordinates
        private int dimX, dimY;
        
        private int fulldimX;

        //Cartesian coordinates of last touch point - joystick center is (0,0)
        private int cartX, cartY;
        
        //Polar coordinates of the touch point from joystick center
        private double radial;
        private double angle;
        
        //User coordinates of last touch point
        private int userX, userY;

        //Offset co-ordinates (used when touch events are received from parent's coordinate origin)
        private int offsetX;
        private int offsetY;

        private double sizefactor=1.0;
        
        private TouchEventWrapper mWrap = TouchEventWrapper.newInstance();
        // =========================================
        // Constructors
        // =========================================

        public JoystickView(Context context) {
                super(context);
                initJoystickView();
        }

        public JoystickView(Context context, AttributeSet attrs) {
                super(context, attrs);
                initJoystickView();
        }

        public JoystickView(Context context, AttributeSet attrs, int defStyle) {
                super(context, attrs, defStyle);
                initJoystickView();
        }

        // =========================================
        // Initialization
        // =========================================

        private void initJoystickView() {
                setFocusable(true);

                dbgPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
                dbgPaint1.setColor(Color.RED);
                dbgPaint1.setStrokeWidth(1);
                dbgPaint1.setStyle(Paint.Style.STROKE);
                
                dbgPaint2 = new Paint(Paint.ANTI_ALIAS_FLAG);
                dbgPaint2.setColor(Color.GREEN);
                dbgPaint2.setStrokeWidth(1);
                dbgPaint2.setStyle(Paint.Style.STROKE);
                
                bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
                bgPaint.setColor(0xA0888888);
                bgPaint.setStrokeWidth(1);
                bgPaint.setStyle(Paint.Style.FILL_AND_STROKE);

                handlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
                handlePaint.setColor(0xB0444444);
                handlePaint.setStrokeWidth(1);
                handlePaint.setStyle(Paint.Style.FILL_AND_STROKE);
                
                butPaintA = new Paint(Paint.ANTI_ALIAS_FLAG);
                butPaintA.setColor(0xA0FF8888);
                butPaintA.setStrokeWidth(1);
                butPaintA.setStyle(Paint.Style.FILL_AND_STROKE);

                butPaintB = new Paint(Paint.ANTI_ALIAS_FLAG);
                butPaintB.setColor(0xA08888FF);
                butPaintB.setStrokeWidth(1);
                butPaintB.setStyle(Paint.Style.FILL_AND_STROKE);

                butClickPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
                butClickPaint.setColor(0xA066FF66);
                butClickPaint.setStrokeWidth(1);
                butClickPaint.setStyle(Paint.Style.FILL_AND_STROKE);

                innerPadding = 10;
                
                setMovementRange(256);
                setMoveResolution(1.0f);
                setClickThreshold(0.4f);
                setYAxisInverted(true);
                setUserCoordinateSystem(COORDINATE_CARTESIAN);
                setAutoReturnToCenter(true);
        }

        public void setAutoReturnToCenter(boolean autoReturnToCenter) {
                this.autoReturnToCenter = autoReturnToCenter;
        }
        
        public boolean isAutoReturnToCenter() {
                return autoReturnToCenter;
        }
        
        public void setUserCoordinateSystem(int userCoordinateSystem) {
                if (userCoordinateSystem < COORDINATE_CARTESIAN || movementConstraint > COORDINATE_DIFFERENTIAL)
                        Log.e(TAG, "invalid value for userCoordinateSystem");
                else
                        this.userCoordinateSystem = userCoordinateSystem;
        }
        
        public int getUserCoordinateSystem() {
                return userCoordinateSystem;
        }
        
        public void setMovementConstraint(int movementConstraint) {
                if (movementConstraint < CONSTRAIN_BOX || movementConstraint > CONSTRAIN_CIRCLE)
                        Log.e(TAG, "invalid value for movementConstraint");
                else
                        this.movementConstraint = movementConstraint;
        }
        
        public int getMovementConstraint() {
                return movementConstraint;
        }
        
        public boolean isYAxisInverted() {
                return yAxisInverted;
        }
        
        public void setYAxisInverted(boolean yAxisInverted) {
                this.yAxisInverted = yAxisInverted;
        }
        
        /**
         * Set the pressure sensitivity for registering a click
         * @param clickThreshold threshold 0...1.0f inclusive. 0 will cause clicks to never be reported, 1.0 is a very hard click
         */
        public void setClickThreshold(float clickThreshold) {
                if (clickThreshold < 0 || clickThreshold > 1.0f)
                        Log.e(TAG, "clickThreshold must range from 0...1.0f inclusive");
                else
                        this.clickThreshold = clickThreshold;
        }
        
        public float getClickThreshold() {
                return clickThreshold;
        }
        
        public void setMovementRange(float movementRange) {
                this.movementRange = movementRange;
        }
        
        public float getMovementRange() {
                return movementRange;
        }
        
        public void setMoveResolution(float moveResolution) {
                this.moveResolution = moveResolution;
        }
        
        public float getMoveResolution() {
                return moveResolution;
        }
        
        public void setTransparency(int val) {
            bgPaint.setAlpha(255-val);
            handlePaint.setAlpha(255-val);
            butPaintA.setAlpha(255-val);
            butPaintB.setAlpha(255-val);
            butClickPaint.setAlpha(255-val);
        }
        
        public void setSize(int val) {
        	sizefactor = (((double)val+1)/6d) - ((val-5)*0.12);
        }
        
        // =========================================
        // Public Methods 
        // =========================================

        public void setOnJostickMovedListener(JoystickMovedListener listener) {
                this.moveListener = listener;
        }
        
        public void setOnJostickClickedListener(JoystickClickedListener listener) {
                this.clickListener = listener;
        }
        
        // =========================================
        // Drawing Functionality 
        // =========================================

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
                // Here we make sure that we have a perfect circle
                int measuredWidth = measure(widthMeasureSpec);
                int measuredHeight = measure(heightMeasureSpec);
                setMeasuredDimension(measuredWidth, measuredHeight);
        }

        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
                super.onLayout(changed, left, top, right, bottom);

                int d = Math.min(getMeasuredWidth(), getMeasuredHeight());

                fulldimX = getMeasuredWidth();

                dimX = d;
                dimY = d;

                cX = d / 2;
                cY = d / 2;

                buttonRadius = (int)((d * 0.15) * sizefactor);
                
                cbutAX = fulldimX - (int)(buttonRadius*4.3);
                cbutAY = cY;
                
                cbutBX = (int)(cbutAX + (buttonRadius*3)); 
                cbutBY = cY;
                
                bgRadius = (int)(dimX/2 - innerPadding);
                handleRadius = (int)(d * 0.25);
                handleInnerBoundaries = handleRadius;
                movementRadius = Math.min(cX, cY) - handleInnerBoundaries;
        }

        private int measure(int measureSpec) {
                int result = 0;
                // Decode the measurement specifications.
                int specMode = MeasureSpec.getMode(measureSpec);
                int specSize = MeasureSpec.getSize(measureSpec);
                if (specMode == MeasureSpec.UNSPECIFIED) {
                        // Return a default size of 200 if no bounds are specified.
                        result = 200;
                } else {
                        // As you want to fill the available space
                        // always return the full available bounds.
                        result = specSize;
                }
                return result;
        }

        @Override
        protected void onDraw(Canvas canvas) {
                canvas.save();
                // Draw the background
                canvas.drawCircle(cX, cY, bgRadius, bgPaint);

                // Draw the handle
                handleX = touchX + cX;
                handleY = touchY + cY;
                canvas.drawCircle(handleX, handleY, handleRadius, handlePaint);
                
                // Draw the buttons
                if (clickedA) {
                	canvas.drawCircle(cbutAX, cbutAY, buttonRadius, butClickPaint);                
                } else {
                	canvas.drawCircle(cbutAX, cbutAY, buttonRadius, butPaintA);
                }

                // Draw the buttons
                if (clickedB) {
                	canvas.drawCircle(cbutBX, cbutBY, buttonRadius, butClickPaint);
                } else {
                	canvas.drawCircle(cbutBX, cbutBY, buttonRadius, butPaintB);
                }

                if (D) {
                        canvas.drawRect(1, 1, getMeasuredWidth()-1, getMeasuredHeight()-1, dbgPaint1);
                        
                        canvas.drawCircle(handleX, handleY, 3, dbgPaint1);
                        
                        if ( movementConstraint == CONSTRAIN_CIRCLE ) {
                                canvas.drawCircle(cX, cY, this.movementRadius, dbgPaint1);
                        }
                        else {
                                canvas.drawRect(cX-movementRadius, cY-movementRadius, cX+movementRadius, cY+movementRadius, dbgPaint1);
                        }
                        
                        //Origin to touch point
                        canvas.drawLine(cX, cY, handleX, handleY, dbgPaint2);
                        
                        int baseY = (int) (touchY < 0 ? cY + handleRadius : cY - handleRadius);
                        canvas.drawText(String.format("%s (%.0f,%.0f)", TAG, touchX, touchY), handleX-20, baseY-7, dbgPaint2);
                        canvas.drawText("("+ String.format("%.0f, %.1f", radial, angle * 57.2957795) + (char) 0x00B0 + ")", handleX-20, baseY+15, dbgPaint2);
                }

//              Log.d(TAG, String.format("touch(%f,%f)", touchX, touchY));
//              Log.d(TAG, String.format("onDraw(%.1f,%.1f)\n\n", handleX, handleY));
                canvas.restore();
        }

        // Constrain touch within a box
        private void constrainBox() {
                touchX = Math.max(Math.min(touchX, movementRadius), -movementRadius);
                touchY = Math.max(Math.min(touchY, movementRadius), -movementRadius);
        }

        // Constrain touch within a circle
        private void constrainCircle() {
                float diffX = touchX;
                float diffY = touchY;
                double radial = Math.sqrt((diffX*diffX) + (diffY*diffY));
                if ( radial > movementRadius ) {
                        touchX = (int)((diffX / radial) * movementRadius);
                        touchY = (int)((diffY / radial) * movementRadius);
                }
        }
        
        public void setPointerId(int id) {
                this.pointerId = id;
        }
        
        private void setPointerIdButtonA(int id) {
        	this.pointerId_butA = id;
        }

        private void setPointerIdButtonB(int id) {
        	this.pointerId_butB = id;
        }
        
        
        public int getPointerId() {
                return pointerId;
        }
        
        private boolean inButtonA(int x, int y) {
        	if ( (x <= cbutAX+buttonRadius) && (x >= cbutAX-buttonRadius) ) {
        		if ((y <= cbutAY+buttonRadius) && (y >= cbutAY-buttonRadius)) {
        			return true;
        		}
        	}
        	return false;
        }

        private boolean inButtonB(int x, int y) {
        	if ( (x <= cbutBX+buttonRadius) && (x >= cbutBX-buttonRadius) ) {
        		if ((y <= cbutBY+buttonRadius) && (y >= cbutBY-buttonRadius)) {
        			return true;
        		}
        	}
        	return false;
        }

        @Override
        public boolean onTouchEvent(MotionEvent ev) {
        	final int action = ev.getAction();
        	final int pointerIndex = ((action & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT);
            final int pId = mWrap.getPointerId(ev,pointerIndex);
            	switch (action & MotionEvent.ACTION_MASK) {
                    case MotionEvent.ACTION_MOVE:
                   		return processMoveEvent(ev);
                    case MotionEvent.ACTION_CANCEL: 
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_POINTER_UP: {
                    	if ( pId == this.pointerId ) {
                    		if ( (pointerId != INVALID_POINTER_ID) && clickedJoy) {
//                              Log.d(TAG, "ACTION_UP");
                                returnHandleToCenter();
                                clickedJoy = false;
                                setPointerId(INVALID_POINTER_ID);
                                return true;
                        	}
                        } else if ( pId == this.pointerId_butA ) {
                        	if ( (pointerId_butA != INVALID_POINTER_ID) && clickedA) {
                        		clickedA = false;
                        		setPointerIdButtonA(INVALID_POINTER_ID);
                        		invalidate();
                        		if (clickListener != null) {
                        			clickListener.OnReleased(0);
                        		}
                        		return true;
                        	}
                        } else if ( pId == this.pointerId_butB ) {
                        	if ( (pointerId_butB != INVALID_POINTER_ID) && clickedB) {
                        		clickedB = false;
                        		setPointerIdButtonB(INVALID_POINTER_ID);
                        		invalidate();
                        		if (clickListener != null) {
                        			clickListener.OnReleased(1);
                        		}
                        		return true;
                        	}
                        }
                        break;
                    }
                    case MotionEvent.ACTION_DOWN: 
                    case MotionEvent.ACTION_POINTER_DOWN: {
                    	int x = (int) mWrap.getX(ev,pointerIndex);
                        int y = (int) mWrap.getY(ev,pointerIndex);
                        if ( (x >= offsetX && x < offsetX + dimX )&& !clickedJoy) {
                            if ( pointerId == INVALID_POINTER_ID ) {
                        		// pointer within joystick
                                setPointerId(pId);
//                              Log.d(TAG, "ACTION_DOWN: " + getPointerId());
                                clickedJoy=true;
                                return true;
                            }
                        } else if (inButtonA(x,y) && !clickedA) {
                        	if (pointerId_butA == INVALID_POINTER_ID) {
                        		// pointer within A button
                        		setPointerIdButtonA(pId);
                        		clickedA=true;
                        		invalidate();
                        		if (clickListener != null) {
                        			clickListener.OnClicked(0);
                        		}
                        		return true;
                        	}
                        } else if (inButtonB(x,y) && !clickedB) {
                        	if (pointerId_butB == INVALID_POINTER_ID) {
                        		setPointerIdButtonB(pId);
                        		clickedB=true;
                        		invalidate();
                        		if (clickListener != null) {
                        			clickListener.OnClicked(1);
                        		}
                        		return true;
                        	}
                        }
                        break;
                    }
            }
            return false;
        }
        
        private boolean processMoveEvent(MotionEvent ev) {
                if ( pointerId != INVALID_POINTER_ID ) {
                        final int pointerIndex = mWrap.findPointerIndex(ev,pointerId);
                        
                        // Translate touch position to center of view
                        float x = mWrap.getX(ev,pointerIndex);
                        touchX = x - cX - offsetX;
                        float y = mWrap.getY(ev,pointerIndex);
                        touchY = y - cY - offsetY;

//              Log.d(TAG, String.format("ACTION_MOVE: (%03.0f, %03.0f) => (%03.0f, %03.0f)", x, y, touchX, touchY));
                
                        reportOnMoved();
                        invalidate();
                        
                       // touchPressure = ev.getPressure(pointerIndex);
                       // reportOnPressure();
                        
                        return true;
                }
                return false;
        }

        private void reportOnMoved() {
                if ( movementConstraint == CONSTRAIN_CIRCLE )
                        constrainCircle();
                else
                        constrainBox();

                calcUserCoordinates();

                if (moveListener != null) {
                        boolean rx = Math.abs(touchX - reportX) >= moveResolution;
                        boolean ry = Math.abs(touchY - reportY) >= moveResolution;
                        if (rx || ry) {
                                this.reportX = touchX;
                                this.reportY = touchY;
                                
//                              Log.d(TAG, String.format("moveListener.OnMoved(%d,%d)", (int)userX, (int)userY));
                                moveListener.OnMoved(userX, userY);
                        }
                }
        }

        private void calcUserCoordinates() {
                //First convert to cartesian coordinates
                cartX = (int)(touchX / movementRadius * movementRange);
                cartY = (int)(touchY / movementRadius * movementRange);
                
                radial = Math.sqrt((cartX*cartX) + (cartY*cartY));
                angle = Math.atan2(cartY, cartX);
                
                //Invert Y axis if requested
                if ( !yAxisInverted )
                        cartY  *= -1;
                
                if ( userCoordinateSystem == COORDINATE_CARTESIAN ) {
                        userX = cartX;
                        userY = cartY;
                }
                else if ( userCoordinateSystem == COORDINATE_DIFFERENTIAL ) {
                        userX = cartY + cartX / 4;
                        userY = cartY - cartX / 4;
                        
                        if ( userX < -movementRange )
                                userX = (int)-movementRange;
                        if ( userX > movementRange )
                                userX = (int)movementRange;

                        if ( userY < -movementRange )
                                userY = (int)-movementRange;
                        if ( userY > movementRange )
                                userY = (int)movementRange;
                }
                
        }
        
        //Simple pressure click
  /*      private void reportOnPressure() {
//              Log.d(TAG, String.format("touchPressure=%.2f", this.touchPressure));
                if ( clickListener != null ) {
                        if ( clicked && touchPressure < clickThreshold ) {
                                clickListener.OnReleased();
                                this.clicked = false;
//                              Log.d(TAG, "reset click");
                                invalidate();
                        }
                        else if ( !clicked && touchPressure >= clickThreshold ) {
                                clicked = true;
                                clickListener.OnClicked();
//                              Log.d(TAG, "click");
                                invalidate();
                                performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
                        }
                }
        } */

        private void returnHandleToCenter() {
                if ( autoReturnToCenter ) {
                        final int numberOfFrames = 5;
                        final double intervalsX = (0 - touchX) / numberOfFrames;
                        final double intervalsY = (0 - touchY) / numberOfFrames;

                        for (int i = 0; i < numberOfFrames; i++) {
                                final int j = i;
                                postDelayed(new Runnable() {
                                        public void run() {
                                                touchX += intervalsX;
                                                touchY += intervalsY;
                                                
                                                reportOnMoved();
                                                invalidate();
                                                
                                                if (moveListener != null && j == numberOfFrames - 1) {
                                                        moveListener.OnReturnedToCenter();
                                                }
                                        }
                                }, i * 40);
                        }

                        if (moveListener != null) {
                                moveListener.OnReleased();
                        }
                }
        }

        public void setTouchOffset(int x, int y) {
                offsetX = x;
                offsetY = y;
        }
}